﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;

namespace CannedHeater.Model.Utilities.Pachube
{
    /// <summary>
    /// Parsing an XML document from the Pachube Service
    /// ParsedData is cached - although this feature is not yet needed in the CannedHeater project
    /// </summary>
    public class PachubeDocReader
    {
        #region Locally used variables
        /// <summary>
        /// Whether the document has been parsed or not
        /// </summary>
        private bool _isParsed = false; // Don't parse twice

        /// <summary>
        /// The raw unparsed document
        /// </summary>
        private XDocument _rawDocument;

        /// <summary>
        /// Associates each data block tag with it's parsed data
        /// </summary>
        private Dictionary<string, PachubeData> _parsedData;

        /// <summary>
        /// The request to perform before parsing
        /// </summary>
        private PachubeRequest _req;
        #endregion

        #region Types
        /// <summary>
        /// Result status
        /// </summary>
        public enum StatusType
        {
            /// <summary>
            /// Successfull parsing
            /// </summary>
            Success,
            /// <summary>
            /// Status Type to indicate an internal error in PachubeRequest
            /// </summary>
            PachubeError,
            /// <summary>
            /// Pachube document could not be parsed - probably malformed
            /// </summary>
            Malformed
        }

        /// <summary>
        /// The result of parsing is "returned" using these event args
        /// </summary>
        public class PachubeDataParsedArgs : EventArgs
        {
            /// <summary>
            /// Parsing result status
            /// </summary>
            public StatusType Status { get; private set; } 

            /// <summary>
            /// Custom error message
            /// </summary>
            public string ErrorMsg { get; private set; }

            /// <summary>
            /// Represents an internal PachubeRequest error, if present
            /// </summary>
            public PachubeRequest.FetchDataArgs PachubeError { get; private set; }

            /// <summary>
            /// Parsed data - the actual result
            /// </summary>
            public Dictionary<string, PachubeData> Data { get; private set; }

            /// <summary>
            /// Reporting internal PachubeRequest error
            /// </summary>
            /// <param name="pArgs">PachubeRequest error arguments</param>
            public PachubeDataParsedArgs(PachubeRequest.FetchDataArgs pArgs)
            {
                Status = StatusType.PachubeError;
                PachubeError = pArgs;
            }

            /// <summary>
            /// Create args for successfull request
            /// </summary>
            /// <param name="data">Dictionary mapping data tags to PachubeData objects</param>
            public PachubeDataParsedArgs(Dictionary<string, PachubeData> data)
            {
                Status = StatusType.Success;
                Data = data;
            }

            /// <summary>
            /// Create args for unsuccessfull request
            /// </summary>
            /// <param name="errorType">Status type representing the error</param>
            /// <param name="errorMsg">Custom error message</param>
            public PachubeDataParsedArgs(StatusType errorType, string errorMsg)
            {
                Status = errorType;
                ErrorMsg = errorMsg;
            }
        }
        #endregion

        #region Events
        public delegate void PachubeDataParsedHandler(object sender, PachubeDataParsedArgs args);

        /// <summary>
        /// Raised when document has been parsed, or possibly case of error
        /// </summary>
        public event PachubeDataParsedHandler PachubeDataParsed;
        #endregion

        #region Constructors
        /// <summary>
        /// Prepare doc reader to execute request before parsing (Start parsing afterwards by call to ExecPachubeRequest)
        /// </summary>
        /// <param name="req">Pachube Request to execute before parsing</param>
        public PachubeDocReader(PachubeRequest req)
        {
            _req = req;
        }

        /// <summary>
        /// Parse provided document, immediatly or later (ParseDoc)
        /// </summary>
        /// <param name="doc">Pachube document to parse</param>
        /// <param name="parseNow">Start parsing immediatly, or wait for explicit ParseDoc call</param>
        public PachubeDocReader(XDocument doc, bool parseNow = true)
        {
            _rawDocument = doc;
            if (parseNow) ParseDoc(doc, false); // Using exceptions since parsing done synchronously
        }
        #endregion

        #region Ways to start parsing
        /// <summary>
        /// Execute the pachube request supplied by constructor
        /// </summary>
        /// <remarks>Precondition: Pachube Request supplied using constructor</remarks>
        public void ExecPachubeRequest()
        {
            _req.FetchDataCompleted += new PachubeRequest.FetchDataHandler(req_FetchDataCompleted);
            _req.FetchRawData();
        }

        
        /// <summary>
        /// After completed request, parses document on success, or raises a pachube error
        /// </summary>
        /// <remarks>Using events to return result, rather than exceptions, since this parsing is done async.</remarks>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void req_FetchDataCompleted(object sender, PachubeRequest.FetchDataArgs args)
        {
            if (args.Status == PachubeRequest.StatusType.Success)
            {
                ParseDoc(args.Doc, true); // Using events
            }
            else
            {
                RaisePachubeDataParsed(new PachubeDataParsedArgs(args));
            }
        }

        /// <summary>
        /// Parse document
        /// </summary>
        /// <remarks>Precondition: Document is specified by constructor</remarks>
        /// <exception cref="CannedHeater.Model.Utilities.Pachube.PachubeDocParseException">In case of unsuccessful parse</exception>
        public void ParseDoc()
        {
            if (!_isParsed) ParseDoc(_rawDocument, false); // Parsing in sync, so using exceptions rather than events
        }

        /// <summary>
        /// Parse document
        /// </summary>
        /// <param name="doc">Pachube document to parse</param>
        /// <exception cref="CannedHeater.Model.Utilities.Pachube.PachubeDocParseException">In case of unsuccessful parse</exception>
        public void ParseDoc(XDocument doc)
        {
            ParseDoc(doc, false); // Parsing in sync, so using exceptions rather than events
        }
        #endregion
        
        /// <summary>
        /// The actual parsing method, parsing a pachube xml document
        /// </summary>
        /// <param name="doc">Pachube document to parse</param>
        /// <param name="useEvents">Use events rather than exceptions - use for async parsing</param>
        /// <exception cref="CannedHeater.Model.Utilities.Pachube.PachubeDocParseException">In case of unsuccessful parse, if useEvents is false</exception>
        private void ParseDoc(XDocument doc, bool useEvents)
        {
            Dictionary<string, PachubeData> parsedData = new Dictionary<string, PachubeData>(); // Result holder

            // Error if no root of xml document
            if (doc.Root == null)
            {
                if (!useEvents)
                    throw new PachubeDocParseException("Invalid data from Pachube");

                Error(StatusType.Malformed, "Invalid data from Pachube");
                return;
            }

            XNamespace ns = doc.Root.GetDefaultNamespace(); // Get the namespace of the pachube document

            XElement env = doc.Root.Element(ns + "environment");

            // Error if no environment xml tag
            if (env == null)
            {
                if (!useEvents)
                    throw new PachubeDocParseException("Invalid data from Pachube");

                Error(StatusType.Malformed, "Invalid data from Pachube");
                return;
            }

            // Get the data elements
            IEnumerable<XElement> dataElements = from dataitem
                                                     in env.Elements(ns + "data")
                                                 where dataitem.Attribute("id") != null
                                                 select dataitem;

            // Parse each data element
            foreach (XElement data in dataElements)
            {
                // For holding results
                SerializableKeyValuePair<DateTime, double>[] timeValuePairs;
                SerializableKeyValuePair<DateTime, double> currentValue;

                // Parse datapoints
                XElement datapoints = data.Element(ns + "datapoints");
                if (datapoints != null)
                {
                    double temp;

                    // Get all history in pairs
                    IEnumerable<SerializableKeyValuePair<DateTime, double>> SerializableKeyValuePairs =
                        from datapointvalue in datapoints.Elements(ns + "value")
                        where datapointvalue.Attribute("at") != null && double.TryParse(datapointvalue.Value, out temp) && double.Parse(datapointvalue.Value) >= -273.16
                        let pair =
                            new SerializableKeyValuePair<DateTime, double>(DateTime.Parse(datapointvalue.Attribute("at").Value),
                                                                           double.Parse(datapointvalue.Value))
                        select pair;

                    // Convert pairs to array
                    timeValuePairs = SerializableKeyValuePairs.ToArray();
                }else
                {
                    timeValuePairs = new SerializableKeyValuePair<DateTime, double>[0];
                }

                // Get current value
                try
                {
                    currentValue = (from curValue in data.Elements(ns + "current_value")
                                    where curValue.Attribute("at") != null
                                    select
                                        new SerializableKeyValuePair<DateTime, double>(
                                        DateTime.Parse(curValue.Attribute("at").Value),
                                        double.Parse(curValue.Value))).First();
                }catch(Exception)
                {
                    // Unknown time (no current value was set in document)
                    currentValue = new SerializableKeyValuePair<DateTime, double>(DateTime.Now, Double.NaN);
                }

                // Get max/min value
                double maxValue = (data.Element(ns + "max_value") == null)
                                      ? Double.NaN
                                      : double.Parse(data.Element(ns + "max_value").Value);
                double minValue = (data.Element(ns + "min_value") == null)
                                      ? Double.NaN
                                      : double.Parse(data.Element(ns + "min_value").Value);

                // Get unit
                XElement unit_el = data.Element(ns + "unit");
                string unit = ((unit_el == null) || unit_el.Attribute("symbol") == null)
                                  ? null
                                  : unit_el.Attribute("symbol").Value;

                string id = data.Attribute("id").Value; // Safe - only parsed if id != null, see LINQ query

                // Add the parsed data to result dictionary
                parsedData.Add(id, new PachubeData(currentValue, maxValue, minValue, unit, timeValuePairs));
            }

            _parsedData = parsedData; // Set the parsed data to be retrieved from outside
            _isParsed = true;

            // "Return" result using an event if told to
            if ( useEvents ) RaisePachubeDataParsed(new PachubeDataParsedArgs(parsedData));
        }

        #region Access Parsed Data
        /// <summary>
        /// Retrieve a PachubeData object for the data represented by the data tag <paramref name="id"/>
        /// </summary>
        /// <param name="id">The data tag to retrieve data for</param>
        /// <returns>The data associated with the data tag <paramref name="id"/></returns>
        public PachubeData GetPachubeData(string id)
        {
            if (_parsedData.ContainsKey(id))
            {
                return _parsedData[id];
            }
            return null;
        }
        #endregion

        #region Helpers
        private void RaisePachubeDataParsed(PachubeDataParsedArgs args)
        {
            if (PachubeDataParsed != null)
                PachubeDataParsed(this, args);
        }

        /// <summary>
        /// Raising error event with specified type and message
        /// </summary>
        /// <param name="type">Error status type</param>
        /// <param name="msg">Custom error message</param>
        private void Error(StatusType type, string msg)
        {
            RaisePachubeDataParsed(new PachubeDataParsedArgs(type, msg));
        }
        #endregion

    }

    /// <summary>
    /// Exception used when document could not be parsed
    /// </summary>
    public class PachubeDocParseException : Exception
    {
        public PachubeDocParseException(string msg) : base(msg)
        {
            
        }
    }
}
